/**
 * 
 */
package org.swing.utility.input.component;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.Iterator;

import javax.swing.BoundedRangeModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.swing.utility.input.common.MathUtils;


/**
 * @author  Le Quynh Nhu
 *
 */
public class JRangeSlider extends JComponent implements MouseListener, MouseMotionListener, KeyListener {
	/*
	 * NOTE: This is a modified version of the original class distributed by Ben Bederson, Jesse Grosjean, and
	 * Jon Meyer as part of an HCIL Tech Report. It is modified to allow both vertical and horitonal modes. It
	 * also fixes a bug with offset on the buttons. Also fixed a bug with rendering using (x,y) instead of
	 * (0,0) as origin. Also modified to render arrows as a series of lines rather than as a GeneralPath. Also
	 * modified to fix rounding errors on toLocal and toScreen.
	 * 
	 * With inclusion in prefuse, this class has been further modified to use a bounded range model, support
	 * keyboard commands and more extensize parameterization of rendering/appearance options. Furthermore, a
	 * stub method has been introduced to allow subclasses to perform custom rendering within the slider
	 * through.
	 * 
	 * With inclusion into r3, this class has been modified to provide more flexibility in rendering, such as
	 * having the option of not using the 3D highlights.
	 */

	/**
	 * Listener that fires a change event when it receives change event from the slider list model.
	 */
	protected class RangeSliderChangeListener implements ChangeListener {
		public void stateChanged(ChangeEvent e) {
			fireChangeEvent();
		}
	}

	protected final static int ARROW_HEIGHT = 4;

	protected final static int ARROW_SZ = 16;

	protected final static int ARROW_WIDTH = 8;

	private static final BasicStroke arrowStroke = new BasicStroke(0);

	public static final int HORIZONTAL = 1;

	public static final int LEFTRIGHT_TOPBOTTOM = 0;

	private static final int PICK_LEFT_OR_TOP = 1;

	private static final int PICK_NONE = 0;

	private static final int PICK_RIGHT_OR_BOTTOM = 3;

	private static final int PICK_THUMB = 2;

	public static final int PREFERRED_BREADTH = 16;

	public static final int PREFERRED_LENGTH = 300;

	public static final int RIGHTLEFT_BOTTOMTOP = 1;

	public static final int VERTICAL = 0;

	private Color arrowColor = new Color(77, 97, 133);

	protected ChangeEvent changeEvent = null;

	protected ChangeListener changeListener;

	protected int direction;

	protected boolean empty;

	protected int increment = 1;

	protected ArrayList listeners = new ArrayList();

	protected int minExtent = 0; // min extent, in pixels

	protected BoundedRangeModel model;

	int mouse;

	protected int orientation;

	int pick;

	int pickOffsetHigh;

	int pickOffsetLow;

	protected Color thumbColor = new Color(150, 180, 220);

	/**
	 * By default, do not paint Win2K highlights.
	 */
	private boolean use3DHighlights = false;

	/**
	 * Create a new range slider.
	 * 
	 * @param model -
	 *            a BoundedRangeModel specifying the slider's range
	 * @param orientation -
	 *            construct a horizontal or vertical slider?
	 * @param direction -
	 *            Is the slider left-to-right/top-to-bottom or right-to-left/bottom-to-top
	 */
	public JRangeSlider(BoundedRangeModel model, int orientation, int direction) {
		super.setFocusable(true);
		this.model = model;
		this.orientation = orientation;
		this.direction = direction;
		setForeground(Color.LIGHT_GRAY);

		this.changeListener = createListener();
		model.addChangeListener(changeListener);

		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
	}

	/**
	 * Create a new range slider.
	 * 
	 * @param minimum -
	 *            the minimum value of the range.
	 * @param maximum -
	 *            the maximum value of the range.
	 * @param lowValue -
	 *            the current low value shown by the range slider's bar.
	 * @param highValue -
	 *            the current high value shown by the range slider's bar.
	 * @param orientation -
	 *            construct a horizontal or vertical slider?
	 */
	public JRangeSlider(int minimum, int maximum, int lowValue, int highValue, int orientation) {
		this(new DefaultBoundedRangeModel(lowValue, highValue, minimum, maximum), orientation,
				LEFTRIGHT_TOPBOTTOM);
	}

	/**
	 * Create a new range slider.
	 * 
	 * @param minimum -
	 *            the minimum value of the range.
	 * @param maximum -
	 *            the maximum value of the range.
	 * @param lowValue -
	 *            the current low value shown by the range slider's bar.
	 * @param highValue -
	 *            the current high value shown by the range slider's bar.
	 * @param orientation -
	 *            construct a horizontal or vertical slider?
	 * @param direction -
	 *            Is the slider left-to-right/top-to-bottom or right-to-left/bottom-to-top
	 */
	public JRangeSlider(int minimum, int maximum, int lowValue, int highValue, int orientation, int direction) {
		this(new DefaultBoundedRangeModel(lowValue, highValue, minimum, maximum), orientation, direction);
	}

	/**
	 * Registers a listener for ChangeEvents.
	 * 
	 * @param cl
	 *            the ChangeListener to add
	 */
	public void addChangeListener(ChangeListener cl) {
		if (!listeners.contains(cl))
			listeners.add(cl);
	}

	/**
	 * Create a listener to relay change events from the bounded range model.
	 * 
	 * @return a ChangeListener to relay events from the range model
	 */
	protected ChangeListener createListener() {
		return new RangeSliderChangeListener();
	}

	/**
	 * Override this method to perform custom painting of the slider trough.
	 * 
	 * @param g
	 *            a Graphics2D context for rendering
	 * @param width
	 *            the width of the slider trough
	 * @param height
	 *            the height of the slider trough
	 */
	protected void customPaint(Graphics2D g, int width, int height) {
		// does nothing in this class
		// subclasses can override to perform custom painting
	}

	/**
	 * Fire a change event to all listeners.
	 */
	protected void fireChangeEvent() {
		repaint();
		if (changeEvent == null) {
			changeEvent = new ChangeEvent(this);
		}
		Iterator iter = listeners.iterator();
		while (iter.hasNext()) {
			((ChangeListener) iter.next()).stateChanged(changeEvent);
		}
	}

	/**
	 * Returns the current "high" value shown by the range slider's bar. The high value meets the constraint
	 * minimum <= lowValue <= highValue <= maximum.
	 */
	public int getHighValue() {
		return model.getValue() + model.getExtent();
	}

	/**
	 * Returns the current "low" value shown by the range slider's bar. The low value meets the constraint
	 * minimum <= lowValue <= highValue <= maximum.
	 */
	public int getLowValue() {
		return model.getValue();
	}

	/**
	 * Gets the maximum possible value for either the low value or the high value.
	 * 
	 * @return the maximum possible range value
	 */
	public int getMaximum() {
		return model.getMaximum();
	}

	/**
	 * Gets the minimum possible value for either the low value or the high value.
	 * 
	 * @return the minimum possible range value
	 */
	public int getMinimum() {
		return model.getMinimum();
	}

	/**
	 * Get the BoundedRangeModel backing this slider.
	 * 
	 * @return the slider's range model
	 */
	public BoundedRangeModel getModel() {
		return model;
	}

	/**
	 * @see java.awt.Component#getPreferredSize()
	 */
	public Dimension getPreferredSize() {
		if (orientation == VERTICAL) {
			return new Dimension(PREFERRED_BREADTH, PREFERRED_LENGTH);
		} else {
			return new Dimension(PREFERRED_LENGTH, PREFERRED_BREADTH);
		}
	}

	/**
	 * Get the slider thumb color. This is the part of the slider between the range resize buttons.
	 * 
	 * @return the slider thumb color
	 */
	public Color getThumbColor() {
		return thumbColor;
	}

	private void grow(int increment) {
		model.setRangeProperties(model.getValue() - increment, model.getExtent() + 2 * increment, model
				.getMinimum(), model.getMaximum(), false);
	}

	// ------------------------------------------------------------------------
	// Rendering

	/**
	 * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent)
	 */
	public void keyPressed(KeyEvent e) {
		int kc = e.getKeyCode();
		boolean v = (orientation == VERTICAL);
		boolean d = (kc == KeyEvent.VK_DOWN);
		boolean u = (kc == KeyEvent.VK_UP);
		boolean l = (kc == KeyEvent.VK_LEFT);
		boolean r = (kc == KeyEvent.VK_RIGHT);

		int minimum = getMinimum();
		int maximum = getMaximum();
		int lowValue = getLowValue();
		int highValue = getHighValue();

		if (v && r || !v && u) {
			if (lowValue - increment >= minimum && highValue + increment <= maximum) {
				grow(increment);
			}
		} else if (v && l || !v && d) {
			if (highValue - lowValue >= 2 * increment) {
				grow(-1 * increment);
			}
		} else if (v && d || !v && l) {
			if (lowValue - increment >= minimum) {
				offset(-increment);
			}
		} else if (v && u || !v && r) {
			if (highValue + increment <= maximum) {
				offset(increment);
			}
		}
	}

	/**
	 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
	 */
	public void keyReleased(KeyEvent e) {
	}

	/**
	 * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent)
	 */
	public void keyTyped(KeyEvent e) {
	}

	/**
	 * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
	 */
	public void mouseClicked(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
	 */
	public void mouseDragged(MouseEvent e) {
		requestFocus();
		int value = (orientation == VERTICAL) ? e.getY() : e.getX();

		int minimum = getMinimum();
		int maximum = getMaximum();
		int lowValue = getLowValue();
		int highValue = getHighValue();

		switch (pick) {
		case PICK_LEFT_OR_TOP:
			int low = toLocal(value - pickOffsetLow);

			if (low < minimum) {
				low = minimum;
			}
			if (low > maximum) {
				low = maximum;
			}
			if (low > highValue - minExtent) {
				low = highValue - minExtent;
			}
			setLowValue(low);
			break;

		case PICK_RIGHT_OR_BOTTOM:
			int high = toLocal(value - pickOffsetHigh);

			if (high < minimum) {
				high = minimum;
			}
			if (high > maximum) {
				high = maximum;
			}
			if (high < lowValue + minExtent) {
				high = lowValue + minExtent;
			}
			setHighValue(high);
			break;

		case PICK_THUMB:
			int dxOrDy = toLocal(value - pickOffsetLow) - lowValue;
			if ((dxOrDy < 0) && ((lowValue + dxOrDy) < minimum)) {
				dxOrDy = minimum - lowValue;
			}
			if ((dxOrDy > 0) && ((highValue + dxOrDy) > maximum)) {
				dxOrDy = maximum - highValue;
			}
			if (dxOrDy != 0) {
				offset(dxOrDy);
			}
			break;
		}
	}

	/**
	 * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
	 */
	public void mouseEntered(MouseEvent e) {
	}

	/**
	 * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
	 */
	public void mouseExited(MouseEvent e) {
	}

	// ------------------------------------------------------------------------
	// Event Handling

	/**
	 * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
	 */
	public void mouseMoved(MouseEvent e) {
		if (orientation == VERTICAL) {
			switch (pickHandle(e.getY())) {
			case PICK_LEFT_OR_TOP:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_RIGHT_OR_BOTTOM:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_THUMB:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_NONE:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			}
		} else {
			switch (pickHandle(e.getX())) {
			case PICK_LEFT_OR_TOP:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_RIGHT_OR_BOTTOM:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_THUMB:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			case PICK_NONE:
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				break;
			}
		}
	}

	/**
	 * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
	 */
	public void mousePressed(MouseEvent e) {
		if (orientation == VERTICAL) {
			pick = pickHandle(e.getY());
			pickOffsetLow = e.getY() - toScreen(getLowValue());
			pickOffsetHigh = e.getY() - toScreen(getHighValue());
			mouse = e.getY();
		} else {
			pick = pickHandle(e.getX());
			pickOffsetLow = e.getX() - toScreen(getLowValue());
			pickOffsetHigh = e.getX() - toScreen(getHighValue());
			mouse = e.getX();
		}
		repaint();
	}

	/**
	 * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
	 */
	public void mouseReleased(MouseEvent e) {
		pick = PICK_NONE;
		repaint();
	}

	private void offset(int dxOrDy) {
		model.setValue(model.getValue() + dxOrDy);
	}

	/**
	 * Adds Windows2K type 3D lighting effects
	 */
	protected void paint3DRectLighting(Graphics2D g2, int x, int y, int width, int height) {
		if (use3DHighlights) {
			g2.setColor(Color.white);
			g2.drawLine(x + 1, y + 1, x + 1, y + height - 1);
			g2.drawLine(x + 1, y + 1, x + width - 1, y + 1);
			g2.setColor(Color.gray);
			g2.drawLine(x + 1, y + height - 1, x + width - 1, y + height - 1);
			g2.drawLine(x + width - 1, y + 1, x + width - 1, y + height - 1);
			g2.setColor(Color.darkGray);
			g2.drawLine(x, y + height, x + width, y + height);
			g2.drawLine(x + width, y, x + width, y + height);
		}
	}

	/**
	 * This draws an arrow as a series of lines within the specified box. The last boolean specifies whether
	 * the point should be at the right/bottom or left/top.
	 */
	protected void paintArrow(Graphics2D g2, double x, double y, int w, int h, boolean topDown) {
		int intX = (int) (x + 0.5);
		int intY = (int) (y + 0.5);

		if (orientation == VERTICAL) {
			if (w % 2 == 0) {
				w = w - 1;
			}

			if (topDown) {
				for (int i = 0; i < (w / 2 + 1); i++) {
					g2.drawLine(intX + i, intY + i, intX + w - i - 1, intY + i);
				}
			} else {
				for (int i = 0; i < (w / 2 + 1); i++) {
					g2.drawLine(intX + w / 2 - i, intY + i, intX + w - w / 2 + i - 1, intY + i);
				}
			}
		} else {
			if (h % 2 == 0) {
				h = h - 1;
			}

			// do this for vertical also (ronyeh)
			final int x_1 = intX + 1;
			final int h_1 = h + 1;
			final int h_over_2_plus_1 = MathUtils.rint(h / 2.0) + 1;
			final int h_1_5_minus_1 = MathUtils.rint(1.5 * h) - 1;
			final int x_w_minus_1 = intX + w - 1;
			final int x_w_minus_2 = intX + w - 2;

			if (topDown) {
				// leftmost
				g2.drawLine(intX, h_over_2_plus_1, x_w_minus_2, h);
				g2.drawLine(intX, h_1_5_minus_1, x_w_minus_2, h_1);

				// middle
				g2.drawLine(x_1, h_over_2_plus_1, x_w_minus_1, h);
				g2.drawLine(x_1, h_1_5_minus_1, x_w_minus_1, h_1);

				// right most
				g2.drawLine(x_1, h_over_2_plus_1 - 1, intX + w, h);
				g2.drawLine(x_1, h_1_5_minus_1 + 1, intX + w, h_1);
			} else {
				g2.drawLine(x_w_minus_1, h_over_2_plus_1, x_1, h);
				g2.drawLine(x_w_minus_1, h_1_5_minus_1, x_1, h_1);

				// middle
				g2.drawLine(x_w_minus_2, h_over_2_plus_1, intX, h);
				g2.drawLine(x_w_minus_2, h_1_5_minus_1, intX, h_1);

				// left most
				g2.drawLine(x_w_minus_2, h_over_2_plus_1 - 1, intX - 1, h);
				g2.drawLine(x_w_minus_2, h_1_5_minus_1 + 1, intX - 1, h_1);
			}
		}
	}

	/**
	 * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
	 */
	public void paintComponent(Graphics g) {
		Rectangle bounds = getBounds();
		int width = (int) bounds.getWidth() - 1;
		int height = (int) bounds.getHeight() - 1;

		int min = toScreen(getLowValue());
		int max = toScreen(getHighValue());

		// Paint the full slider if the slider is marked as empty
		if (empty) {
			if (direction == LEFTRIGHT_TOPBOTTOM) {
				min = ARROW_SZ;
				max = (orientation == VERTICAL) ? height - ARROW_SZ : width - ARROW_SZ;
			} else {
				min = (orientation == VERTICAL) ? height - ARROW_SZ : width - ARROW_SZ;
				max = ARROW_SZ;
			}
		}

		final Graphics2D g2 = (Graphics2D) g;
		g2.setColor(getBackground());
		g2.fillRect(0, 0, width, height);

		g2.setColor(getForeground());
		g2.drawRect(0, 0, width, height);

		// Draw arrow and thumb backgrounds
		g2.setStroke(arrowStroke);
		final int thumbExtent = Math.abs(max - min) - 1;

		if (orientation == VERTICAL) {
			if (direction == LEFTRIGHT_TOPBOTTOM) {
				g2.setColor(getForeground());
				g2.fillRect(0, min - ARROW_SZ, width, ARROW_SZ - 1);
				paint3DRectLighting(g2, 0, min - ARROW_SZ, width, ARROW_SZ - 1);

				if (thumbColor != null) {
					g2.setColor(thumbColor);
					g2.fillRect(0, min, width, thumbExtent);
					paint3DRectLighting(g2, 0, min, width, thumbExtent);
				}

				g2.setColor(getForeground());
				g2.fillRect(0, max, width, ARROW_SZ - 1);
				paint3DRectLighting(g2, 0, max, width, ARROW_SZ - 1);

				// Draw arrows
				g2.setColor(arrowColor);
				paintArrow(g2, (width - ARROW_WIDTH) / 2.0, min - ARROW_SZ + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						ARROW_WIDTH, ARROW_HEIGHT, true);
				paintArrow(g2, (width - ARROW_WIDTH) / 2.0, max + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						ARROW_WIDTH, ARROW_HEIGHT, false);
			} else {
				g2.setColor(getForeground());
				g2.fillRect(0, min, width, ARROW_SZ - 1);
				paint3DRectLighting(g2, 0, min, width, ARROW_SZ - 1);

				if (thumbColor != null) {
					g2.setColor(thumbColor);
					g2.fillRect(0, max, width, thumbExtent);
					paint3DRectLighting(g2, 0, max, width, thumbExtent);
				}

				g2.setColor(getForeground());
				g2.fillRect(0, max - ARROW_SZ, width, ARROW_SZ - 1);
				paint3DRectLighting(g2, 0, max - ARROW_SZ, width, ARROW_SZ - 1);

				// Draw arrows
				g2.setColor(arrowColor);
				paintArrow(g2, (width - ARROW_WIDTH) / 2.0, min + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						ARROW_WIDTH, ARROW_HEIGHT, false);
				paintArrow(g2, (width - ARROW_WIDTH) / 2.0, max - ARROW_SZ + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						ARROW_WIDTH, ARROW_HEIGHT, true);
			}
		} else { // HORIZONTAL
			if (direction == LEFTRIGHT_TOPBOTTOM) {
				g2.setColor(getForeground());
				g2.fillRect(min - ARROW_SZ, 0, ARROW_SZ - 1, height);
				paint3DRectLighting(g2, min - ARROW_SZ, 0, ARROW_SZ - 1, height);

				if (thumbColor != null) {
					g2.setColor(thumbColor);
					g2.fillRect(min, 0, thumbExtent, height);
					paint3DRectLighting(g2, min, 0, thumbExtent, height);
				}

				g2.setColor(getForeground());
				g2.fillRect(max, 0, ARROW_SZ - 1, height);
				paint3DRectLighting(g2, max, 0, ARROW_SZ - 1, height);

				// Draw arrows
				g2.setColor(arrowColor);
				paintArrow(g2, min - ARROW_SZ + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						(height - ARROW_WIDTH) / 2.0, ARROW_HEIGHT, ARROW_WIDTH, true);
				paintArrow(g2, max + (ARROW_SZ - ARROW_HEIGHT) / 2.0, (height - ARROW_WIDTH) / 2.0,
						ARROW_HEIGHT, ARROW_WIDTH, false);
			} else {
				g2.setColor(getForeground());
				g2.fillRect(min, 0, ARROW_SZ - 1, height);
				paint3DRectLighting(g2, min, 0, ARROW_SZ - 1, height);

				if (thumbColor != null) {
					g2.setColor(thumbColor);
					g2.fillRect(max, 0, thumbExtent, height);
					paint3DRectLighting(g2, max, 0, thumbExtent, height);
				}

				g2.setColor(getForeground());
				g2.fillRect(max - ARROW_SZ, 0, ARROW_SZ - 1, height);
				paint3DRectLighting(g2, max - ARROW_SZ, 0, ARROW_SZ - 1, height);

				// Draw arrows
				g2.setColor(arrowColor);
				paintArrow(g2, min + (ARROW_SZ - ARROW_HEIGHT) / 2.0, (height - ARROW_WIDTH) / 2.0,
						ARROW_HEIGHT, ARROW_WIDTH, true);
				paintArrow(g2, max - ARROW_SZ + (ARROW_SZ - ARROW_HEIGHT) / 2.0,
						(height - ARROW_WIDTH) / 2.0, ARROW_HEIGHT, ARROW_WIDTH, false);
			}
		}
		customPaint(g2, width, height);

	}

	private int pickHandle(int xOrY) {
		int min = toScreen(getLowValue());
		int max = toScreen(getHighValue());
		int pick = PICK_NONE;

		if (direction == LEFTRIGHT_TOPBOTTOM) {
			if ((xOrY > (min - ARROW_SZ)) && (xOrY < min)) {
				pick = PICK_LEFT_OR_TOP;
			} else if ((xOrY >= min) && (xOrY <= max)) {
				pick = PICK_THUMB;
			} else if ((xOrY > max) && (xOrY < (max + ARROW_SZ))) {
				pick = PICK_RIGHT_OR_BOTTOM;
			}
		} else {
			if ((xOrY > min) && (xOrY < (min + ARROW_SZ))) {
				pick = PICK_LEFT_OR_TOP;
			} else if ((xOrY <= min) && (xOrY >= max)) {
				pick = PICK_THUMB;
			} else if ((xOrY > (max - ARROW_SZ) && (xOrY < max))) {
				pick = PICK_RIGHT_OR_BOTTOM;
			}
		}

		return pick;
	}

	/**
	 * Removes a listener for ChangeEvents.
	 * 
	 * @param cl
	 *            the ChangeListener to remove
	 */
	public void removeChangeListener(ChangeListener cl) {
		listeners.remove(cl);
	}

	/**
	 * Sets whether this slider is empty.
	 * 
	 * @param empty
	 *            true if set to empty, false otherwise
	 */
	public void setEmpty(boolean empty) {
		this.empty = empty;
		repaint();
	}

	/**
	 * Sets the high value shown by this range slider. This causes the range slider to be repainted and a
	 * ChangeEvent to be fired.
	 * 
	 * @param highValue
	 *            the high value to use
	 */
	public void setHighValue(int highValue) {
		model.setExtent(highValue - model.getValue());
	}

	/**
	 * Sets the low value shown by this range slider. This causes the range slider to be repainted and a
	 * ChangeEvent to be fired.
	 * 
	 * @param lowValue
	 *            the low value to use
	 */
	public void setLowValue(int lowValue) {
		int e = (model.getValue() - lowValue) + model.getExtent();
		model.setRangeProperties(lowValue, e, model.getMinimum(), model.getMaximum(), false);
		model.setValue(lowValue);
	}

	/**
	 * Sets the maximum possible value for either the low value or the high value.
	 * 
	 * @param maximum
	 *            the maximum possible range value
	 */
	public void setMaximum(int maximum) {
		model.setMaximum(maximum);
	}

	/**
	 * Sets the minimum extent (difference between low and high values). This method <strong>does not</strong>
	 * change the current state of the model, but can affect all subsequent interaction.
	 * 
	 * @param minExtent
	 *            the minimum extent allowed in subsequent interaction
	 */
	public void setMinExtent(int minExtent) {
		this.minExtent = minExtent;
	}

	/**
	 * Sets the minimum possible value for either the low value or the high value.
	 * 
	 * @param minimum
	 *            the minimum possible range value
	 */
	public void setMinimum(int minimum) {
		model.setMinimum(minimum);
	}

	/**
	 * Set the BoundedRangeModel backing this slider.
	 * 
	 * @param brm
	 *            the slider range model to use
	 */
	public void setModel(BoundedRangeModel brm) {
		model.removeChangeListener(changeListener);
		model = brm;
		model.addChangeListener(changeListener);
		repaint();
	}

	/**
	 * Set the slider range span.
	 * 
	 * @param lowValue
	 *            the low value of the slider range
	 * @param highValue
	 *            the high value of the slider range
	 */
	public void setRange(int lowValue, int highValue) {
		model.setRangeProperties(lowValue, highValue - lowValue, model.getMinimum(), model.getMaximum(),
				false);
	}

	/**
	 * Set the slider thumb color. This is the part of the slider between the range resize buttons.
	 * 
	 * @param thumbColor
	 *            the slider thumb color
	 */
	public void setThumbColor(Color thumbColor) {
		this.thumbColor = thumbColor;
	}

	/**
	 * Converts from screen coordinates to a range value.
	 */
	protected int toLocal(int xOrY) {
		Dimension sz = getSize();
		int min = getMinimum();
		double scale;
		if (orientation == VERTICAL) {
			scale = (sz.height - (2 * ARROW_SZ)) / (double) (getMaximum() - min);
		} else {
			scale = (sz.width - (2 * ARROW_SZ)) / (double) (getMaximum() - min);
		}

		if (direction == LEFTRIGHT_TOPBOTTOM) {
			return (int) (((xOrY - ARROW_SZ) / scale) + min + 0.5);
		} else {
			if (orientation == VERTICAL) {
				return (int) ((sz.height - xOrY - ARROW_SZ) / scale + min + 0.5);
			} else {
				return (int) ((sz.width - xOrY - ARROW_SZ) / scale + min + 0.5);
			}
		}
	}

	/**
	 * Converts from a range value to screen coordinates.
	 */
	protected int toScreen(int xOrY) {
		Dimension sz = getSize();
		int min = getMinimum();
		double scale;
		if (orientation == VERTICAL) {
			scale = (sz.height - (2 * ARROW_SZ)) / (double) (getMaximum() - min);
		} else {
			scale = (sz.width - (2 * ARROW_SZ)) / (double) (getMaximum() - min);
		}

		// If the direction is left/right_top/bottom then we subtract the min and multiply times
		// scale
		// Otherwise, we have to invert the number by subtracting the value from the height
		if (direction == LEFTRIGHT_TOPBOTTOM) {
			return (int) (ARROW_SZ + ((xOrY - min) * scale) + 0.5);
		} else {
			if (orientation == VERTICAL) {
				return (int) (sz.height - (xOrY - min) * scale - ARROW_SZ + 0.5);
			} else {
				return (int) (sz.width - (xOrY - min) * scale - ARROW_SZ + 0.5);
			}
		}
	}

	/**
	 * Converts from a range value to screen coordinates.
	 */
	protected double toScreenDouble(int xOrY) {
		Dimension sz = getSize();
		int min = getMinimum();
		double scale;
		if (orientation == VERTICAL) {
			scale = (sz.height - (2 * ARROW_SZ)) / (double) (getMaximum() + 1 - min);
		} else {
			scale = (sz.width - (2 * ARROW_SZ)) / (double) (getMaximum() + 1 - min);
		}

		// If the direction is left/right_top/bottom then we subtract the min and multiply times
		// scale
		// Otherwise, we have to invert the number by subtracting the value from the height
		if (direction == LEFTRIGHT_TOPBOTTOM) {
			return ARROW_SZ + ((xOrY - min) * scale);
		} else {
			if (orientation == VERTICAL) {
				return sz.height - (xOrY - min) * scale - ARROW_SZ;
			} else {
				return sz.width - (xOrY - min) * scale - ARROW_SZ;
			}
		}
	}

} // end of class JRangeSlider
